/*  AHB Status register driver
 *
 *  COPYRIGHT (c) 2009 - 2017.
 *  Cobham Gaisler AB.
 *
 *  The license and distribution terms for this file may be
 *  found in the file LICENSE in this distribution or at
 *  http://www.rtems.org/license/LICENSE.
 */

#include <stdint.h>
#include <string.h>
#include <rtems.h>
#include <rtems/bspIo.h>
#include <drvmgr/drvmgr.h>
#include <drvmgr/ambapp_bus.h>

#include <bsp/ahbstat.h>

#define SPIN_IRQ_DECLARE(name)          RTEMS_INTERRUPT_LOCK_DECLARE(, name)
#define SPIN_IRQ_INIT(lock, name)       rtems_interrupt_lock_initialize(lock, name)
#define SPIN_IRQ_LOCK(lock, ctx)        rtems_interrupt_lock_acquire(lock, &(ctx))
#define SPIN_IRQ_UNLOCK(lock, ctx)      rtems_interrupt_lock_release(lock, &(ctx))
#define SPIN_IRQ_LOCK_ISR(lock, ctx)    rtems_interrupt_lock_acquire_isr(lock, &(ctx))
#define SPIN_IRQ_UNLOCK_ISR(lock, ctx)  rtems_interrupt_lock_release_isr(lock, &(ctx))
#define SPIN_IRQ_CTX                    rtems_interrupt_lock_context

#define REG_WRITE(addr, val) (*(volatile uint32_t *)(addr) = (uint32_t)(val))
#define REG_READ(addr) (*(volatile uint32_t *)(addr))

void ahbstat_isr(void *arg);

/* AHB fail interrupt callback to user. This function is declared weak so that
 * the user can define a function pointer variable containing the address
 * responsible for handling errors
 *
 * minor              Index of AHBSTAT hardware
 * regs               Register address of AHBSTAT
 * status             AHBSTAT status register at IRQ
 * failing_address    AHBSTAT Failing address register at IRQ
 *
 * * User return 
 *  0: print error onto terminal with printk and reenable AHBSTAT
 *  1: just re-enable AHBSTAT
 *  2: just print error
 *  3: do nothing, let user do custom handling
 */
int (*ahbstat_error)(
	int minor,
	struct ahbstat_regs *regs,
	uint32_t status,
	uint32_t failing_address
	) __attribute__((weak)) = NULL;

#define AHBSTAT_STS_CE_BIT 9
#define AHBSTAT_STS_NE_BIT 8
#define AHBSTAT_STS_HW_BIT 7
#define AHBSTAT_STS_HM_BIT 3
#define AHBSTAT_STS_HS_BIT 0

#define AHBSTAT_STS_CE (1 << AHBSTAT_STS_CE_BIT)
#define AHBSTAT_STS_NE (1 << AHBSTAT_STS_NE_BIT)
#define AHBSTAT_STS_HW (1 << AHBSTAT_STS_HW_BIT)
#define AHBSTAT_STS_HM (0xf << AHBSTAT_STS_HM_BIT)
#define AHBSTAT_STS_HS (0x7 << AHBSTAT_STS_HS_BIT)

enum { DEVNAME_LEN = 9 };
struct ahbstat_priv {
	struct drvmgr_dev *dev;
	struct ahbstat_regs *regs;
	char devname[DEVNAME_LEN];
	int minor;
	/* Cached error */
	uint32_t last_status;
	uint32_t last_address;
	/* Spin-lock ISR protection */
	SPIN_IRQ_DECLARE(devlock);
};

static int ahbstat_init2(struct drvmgr_dev *dev);

struct drvmgr_drv_ops ahbstat_ops =
{
	.init = {NULL, ahbstat_init2, NULL, NULL},
	.remove = NULL,
	.info = NULL
};

struct amba_dev_id ahbstat_ids[] =
{
	{VENDOR_GAISLER, GAISLER_AHBSTAT},
	{0, 0}		/* Mark end of table */
};

struct amba_drv_info ahbstat_drv_info =
{
	{
		DRVMGR_OBJ_DRV,			/* Driver */
		NULL,				/* Next driver */
		NULL,				/* Device list */
		DRIVER_AMBAPP_GAISLER_AHBSTAT_ID,/* Driver ID */
		"AHBSTAT_DRV",			/* Driver Name */
		DRVMGR_BUS_TYPE_AMBAPP,		/* Bus Type */
		&ahbstat_ops,
		NULL,				/* Funcs */
		0,				/* No devices yet */
		sizeof(struct ahbstat_priv),
	},
	&ahbstat_ids[0]
};

void ahbstat_register_drv (void)
{
	drvmgr_drv_register(&ahbstat_drv_info.general);
}

static int ahbstat_init2(struct drvmgr_dev *dev)
{
	struct ahbstat_priv *priv;
	struct amba_dev_info *ambadev;

	priv = dev->priv;
	if (!priv)
		return DRVMGR_NOMEM;
	priv->dev = dev;

	/* Get device information from AMBA PnP information */
	ambadev = (struct amba_dev_info *)dev->businfo;
	if (ambadev == NULL)
		return DRVMGR_FAIL;
	priv->regs = (struct ahbstat_regs *)ambadev->info.apb_slv->start;
	priv->minor = dev->minor_drv;

	strncpy(&priv->devname[0], "ahbstat0", DEVNAME_LEN);
	priv->devname[7] += priv->minor;
	/*
	 * Initialize spinlock for AHBSTAT Device. It is used to protect user
	 * API calls involivng priv structure from updates in ISR.
	 */
	SPIN_IRQ_INIT(&priv->devlock, priv->devname);

	/* Initialize hardware */
	REG_WRITE(&priv->regs->status, 0);

	/* Install IRQ handler */
	drvmgr_interrupt_register(dev, 0, priv->devname, ahbstat_isr, priv);

	return DRVMGR_OK;
}

void ahbstat_isr(void *arg)
{
	struct ahbstat_priv *priv = arg;
	uint32_t fadr, status;
	int rc;
	SPIN_IRQ_CTX lock_context;

	/* Get hardware status */
	status = REG_READ(&priv->regs->status);
	if ((status & AHBSTAT_STS_NE) == 0)
		return;

	/* IRQ generated by AHBSTAT core... handle it here */

	/* Get Failing address */
	fadr = REG_READ(&priv->regs->failing);

	SPIN_IRQ_LOCK_ISR(&priv->devlock, lock_context);
	priv->last_status = status;
	priv->last_address = fadr;
	SPIN_IRQ_UNLOCK_ISR(&priv->devlock, lock_context);

	/* Let user handle error, default to print the error and reenable HW
	 *
	 * User return 
	 *  0: print error and reenable AHBSTAT
	 *  1: just reenable AHBSTAT
	 *  2: just print error
	 *  3: do nothing
	 */
	rc = 0;
	if (ahbstat_error != NULL)
		rc = ahbstat_error(priv->minor, priv->regs, status, fadr);

	if ((rc & 0x1) == 0) {
		printk("\n### AHBSTAT: %s %s error of size %ld by master %ld"
			" at 0x%08lx\n",
			status & AHBSTAT_STS_CE ? "single" : "non-correctable",
			status & AHBSTAT_STS_HW ? "write" : "read",
			(status & AHBSTAT_STS_HS) >> AHBSTAT_STS_HS_BIT,
			(status & AHBSTAT_STS_HM) >> AHBSTAT_STS_HM_BIT,
			fadr);
	}

	if ((rc & 0x2) == 0) {
		/* Trigger new interrupts */
		REG_WRITE(&priv->regs->status, 0);
	}
}

/* Get Last received AHB Error
 *
 * Return
 *   0: No error received
 *   1: Error Received, last status and address stored into argument pointers
 *  -1: No such AHBSTAT device
 */
int ahbstat_last_error(int minor, uint32_t *status, uint32_t *address)
{
	struct drvmgr_dev *dev;
	struct ahbstat_priv *priv;
	uint32_t last_status;
	uint32_t last_address;
	SPIN_IRQ_CTX lock_context;

	if (drvmgr_get_dev(&ahbstat_drv_info.general, minor, &dev)) {
		return -1;
	}
	priv = (struct ahbstat_priv *)dev->priv;

	/* Read information cached by ISR */
	SPIN_IRQ_LOCK(&priv->devlock, lock_context);
	last_status = REG_READ(&priv->last_status);
	last_address = REG_READ(&priv->last_address);
	SPIN_IRQ_UNLOCK(&priv->devlock, lock_context);

	*status = last_status;
	*address = last_address;

	return (last_status & AHBSTAT_STS_NE) >> AHBSTAT_STS_NE_BIT;
}

/* Get AHBSTAT registers address from minor. NULL returned if no such device */
struct ahbstat_regs *ahbstat_get_regs(int minor)
{
	struct drvmgr_dev *dev;
	struct ahbstat_priv *priv;

	if (drvmgr_get_dev(&ahbstat_drv_info.general, minor, &dev)) {
		return NULL;
	}
	priv = (struct ahbstat_priv *)dev->priv;

	return priv->regs;
}
